<?php declare(strict_types=1);

namespace Reezy\Cors;


use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use function join;

class CorsMiddleware implements MiddlewareInterface
{
    protected $allowedAllOrigins = false;
    protected $allowedOrigins = [];
    protected $allowedMethods = [];
    protected $allowedCredentials = true;
    protected $allowedHeaders = [];
    protected $exposedHeaders = [];
    protected $maxAge = 0;

    /**
     * @var ResponseFactoryInterface
     */
    private $responseFactory;

    public function __construct(ResponseFactoryInterface $responseFactory)
    {

        $config = config('cors');

        $this->allowedAllOrigins = $config['allowed-all-origins'] ?? false;
        $this->allowedOrigins = $config['allowed-origins'] ?? [];
        $this->allowedMethods = $config['allowed-methods'] ?? [];
        $this->allowedHeaders = $config['allowed-headers'] ?? [];
        $this->exposedHeaders = $config['exposed-headers'] ?? [];
        $this->allowedCredentials = $config['allowed-credentials'] ?? false;
        $this->maxAge = $config['max-age'] ?? 0;

        $this->responseFactory = $responseFactory;
    }

    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (!$this->issCorsRequestAndOriginAllowed($request)) {
            return $handler->handle($request);
        }

        $headers = [];

        if ($this->allowedCredentials) {
            // 如果服务端指定了具体的域名而非“*”，那么响应首部中的 Vary 字段的值必须包含 Origin
            $headers['Access-Control-Allow-Origin'] = $request->getHeaderLine('origin');
            $headers['Access-Control-Allow-Credentials'] = 'true';
            $headers['Vary'] = 'Origin';
        } else {
            $headers['Access-Control-Allow-Origin'] = "*";
        }

        if ($this->isPreflightRequest($request)) {
            if (!$this->isPreflightAllowed($request)) {
                return $this->responseFactory->createResponse(403, 'Forbidden');
            }

            $headers['Access-Control-Allow-Methods'] = join(',', $this->allowedMethods);
            $headers['Access-Control-Allow-Headers'] = join(',', $this->allowedHeaders);
            if ($this->maxAge > 0) {
                $headers['Access-Control-Max-Age'] = $this->maxAge;
            }
            $response = $this->responseFactory->createResponse(204, 'OK');
        } else {
            if (!empty($this->exposedHeaders)) {
                $headers['Access-Control-Expose-Headers'] = join(',', $this->exposedHeaders);
            }
            $response = $handler->handle($request);
        }
        foreach ($headers as $key => $value) {
            $response = $response->withHeader($key, $value);
        }
        return $response;
    }

    private function issCorsRequestAndOriginAllowed(ServerRequestInterface $request)
    {
        $origin = $request->getHeaderLine('origin');
        if (empty($origin)) {
            return false;
        }
        $uri = $request->getUri();
        $host = $uri->getScheme() . '://' . $uri->getAuthority();

        return ($origin !== $host) && ($this->allowedAllOrigins || in_array($origin, $this->allowedOrigins));
    }

    private function isPreflightRequest(ServerRequestInterface $request)
    {
        return $request->getMethod() === 'OPTIONS' && $request->hasHeader('Access-Control-Request-Method');
    }

    private function isPreflightAllowed(ServerRequestInterface $request)
    {
        $method = $request->getHeaderLine('Access-Control-Request-Method');
        if (!in_array($method, $this->allowedMethods)) {
            return false;
        }
        $headers = explode(',', $request->getHeaderLine('Access-Control-Request-Headers'));

//        $allowedHeaders = array_merge($this->allowedHeaders, ['content-type']);

        foreach ($headers as $header) {
            if (!in_array($header, $this->allowedHeaders)) {
                return false;
            }
        }
        return true;
    }
}